<?php
// +----------------------------------------------------------------------
// | INPHP
// +----------------------------------------------------------------------
// | Copyright (c) 2021 https://inphp.cc All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( https://opensource.org/licenses/MIT )
// +----------------------------------------------------------------------
// | Author: lulanyin <me@lanyin.lu>
// +----------------------------------------------------------------------
// | 
// |                          数据库连接
// |
// +----------------------------------------------------------------------
namespace Inphp\DB;

use PDO;
use PDOException;

/**
 * 数据库连接类
 * Class Connection
 * @package Inphp\DB
 */
class Connection
{
    /**
     * 是否开启Debug
     * @var bool
     */
    public bool $debug = false;

    /**
     * 日志文件
     * @var string
     */
    private string $log_file = '';
    
    /**
     * 执行的SQL保存文件的目录
     * @var string
     */
    private string $execute_log_dir = '';

    /**
     * 数据库连接配置
     * @var array
     */
    public array $configs = [];

    /**
     * @var PDO[]
     */
    public array $pdo = [];

    /**
     * 表名前缀
     * @var string
     */
    public string $table_prefix = "pre_";

    /**
     * 字符集类型
     * @var string
     */
    public string $charset = 'utf8mb4';

    /**
     * 是否在事务中...
     * @var bool
     */
    public bool $in_transaction = false;

    /**
     * 错误
     * @var array
     */
    private array $error = [
        'error' => '',
        'id'    => 0
    ];

    /**
     * 查询遍历模式
     * @var int
     */
    public int $fetch_model = PDO::FETCH_ASSOC;

    /**
     * 超时时间
     * @var int
     */
    public int $timeout = 5;

    /**
     * 记录重新连接次数
     * @var int
     */
    private int $reconnect_times = 0;

    /**
     * 保留关键字
     * 如果查询语句中，包含单独的关键字，将会被处理为 xxx 替换为 `xxx`
     * @var array
     */
    public array $keywords = [
        'select', 'update', 'delete', 'set', 'from', 'find_in_set', 'where', 'join', 'in', 'on',
        'rank'
    ];

    /**
     * 初始化，可以带配置，也可以不带
     * 支持读写分离
     * Connection constructor.
     * @param array|null $configs
     */
    public function __construct(array|null $configs = null)
    {
        if(is_array($configs))
        {
            //是否开启DEBUG
            $this->debug = $configs['debug'] == true;
            //查询遍历模式
            $this->fetch_model = $configs['fetch_model'] ?? PDO::FETCH_ASSOC;
            //表前缀
            $this->table_prefix = $configs['table_prefix'] ?? '';
            //超时
            $this->timeout = $configs['timeout'] ?? 5;
            //字符集类型
            $this->charset = $configs['charset'] ?? $this->charset;
            //日志文件
            $this->log_file = $configs['log_file'] ?? '';
            //执行日志
            $this->execute_log_dir = $configs['execute_log_dir'] ?? '';
            //是否添加有保留关键字
            if(isset($configs['keywords']))
            {
                //如果是字符串，则使用,分割成数组
                $keywords = is_array($configs['keywords']) ? $configs['keywords'] : explode(",", $configs['keywords']);
                //合并
                $this->keywords = array_merge($this->keywords, $keywords);
            }
            //读
            $read = $configs['read'] ?? [];
            $read = !empty($read) ? $read : ($configs['default'] ?? []);
            $read = !empty($read) ? $read : $configs;
            if(!empty($read))
            {
                $this->init($read, 'read');
            }
            //写
            $write = $configs['write'] ?? [];
            $write = !empty($write) ? $write : ($configs['default'] ?? []);
            $write = !empty($write) ? $write : $configs;
            if(!empty($write))
            {
                $this->init($write, 'write');
            }
            //开发中...
            $dev = $configs['dev'] ?? [];
            $dev = !empty($dev) ? $dev : ($configs['default'] ?? []);
            $dev = !empty($dev) ? $dev : $configs;
            if(!empty($dev))
            {
                $this->init($dev, 'dev');
            }
        }
    }

    /**
     * 初始化配置
     * @param array $config
     * @param string $type
     * @return Connection
     */
    public function init(array $config, string $type = 'read') : Connection
    {
        $this->configs[$type] = $config;
        return $this;
    }

    /**
     * 获取PDO对象
     * @param string $type
     * @param array|null $config
     * @return PDO|false
     */
    public function connect(string $type = 'read', array|null $config = null) : mixed
    {
        if(isset($this->pdo[$type]) && $this->pdo[$type] instanceof PDO)
        {
            return $this->pdo[$type];
        }
        if(is_array($config))
        {
            $this->configs[$type] = $config;
        }
        else
        {
            $config = $this->configs[$type];
        }
        //
        if(
            ($type == 'read' && isset($this->pdo['write']) && $this->pdo['write'] instanceof PDO)
            || ($type == 'write' && isset($this->pdo['read']) && $this->pdo['read'] instanceof PDO)
        )
        {
            if($config == $this->configs[$type == 'read' ? 'write' : 'read'])
            {
                $this->pdo[$type] = $this->pdo[$type == 'read' ? 'write' : 'read'];
                return $this->pdo[$type];
            }
        }
        //取参数
        $host = $config['host'] ?? '127.0.0.1';
        $port = $config['port'] ?? 3306;
        $user = $config['user'] ?? ($config['username'] ?? 'root');
        $pass = $config['pass'] ?? ($config['password'] ?? '12345678');
        $database = $config['database'] ?? ($config['dbname'] ?? 'database');
        $charset = $config['charset'] ?? $this->charset;
        $timeout = $config['timeout'] ?? $this->timeout;
        $timeout = is_numeric($timeout) && $timeout >= 0 ? ceil($timeout) : 5;
        //连接mysql数据库
        $dns = "mysql:host={$host};port={$port};dbname={$database}";
        try {
            $this->pdo[$type] = new PDO($dns, $user, $pass, [
                PDO::ATTR_PERSISTENT => 1,
                PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES '{$charset}';",
                PDO::MYSQL_ATTR_USE_BUFFERED_QUERY => true,
                PDO::ATTR_EMULATE_PREPARES => true,
                PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
                PDO::ATTR_TIMEOUT => $timeout
            ]);
            return $this->pdo[$type];
        }catch (PDOException $exception) {
            //连接数据库失败
            echo "连接数据库失败".PHP_EOL;
            echo $exception->getMessage().PHP_EOL;
            return false;
        }
    }

    /**
     * 重新连接
     * @param string $type
     * @return false|PDO
     */
    public function reconnect(string $type = 'read') : mixed
    {
        if($this->reconnect_times < 10)
        {
            $type = DB::isEnvRelease() ? $type : 'dev';
            return $this->connect($type);
        }
        return false;
    }

    /**
     * 获取PDO对象
     * @param string $type
     * @return false|PDO
     */
    public function getPdo(string $type = 'read') : mixed
    {
        $type = DB::isEnvRelease() ? $type : 'dev';
        return $this->pdo[$type] ?? $this->connect($type);
    }

    /**
     * 获取表前缀
     * @param string $type
     * @return string
     */
    public function getTablePrefix(string $type = 'read') : string
    {
        $type = DB::isEnvRelease() ? $type : 'dev';
        $config = $this->configs[$type] ?? [];
        return $config['table_prefix'] ?? $this->table_prefix;
    }

    /**
     * 获取数据库名
     * @param string $type
     * @return string
     */
    public function getDBName(string $type = 'read') : string
    {
        $type = DB::isEnvRelease() ? $type : 'dev';
        $config = $this->configs[$type] ?? [];
        return $config['database'] ?? ($config['dbname'] ?? '');
    }

    /**
     * 开始事务
     * @return PDO
     */
    public function begin() : mixed
    {
        //判断当前是什么环境，如果声明了是测试环境，统计用dev，否则用write
        $pdo = $this->getPdo('write');
        if(!$this->in_transaction)
        {
            $pdo->beginTransaction();
        }
        $this->in_transaction = true;
        return $pdo;
    }

    /**
     * 事务回滚
     */
    public function rollback()
    {
        if($this->in_transaction)
        {
            $this->getPdo('write')->rollBack();
        }
        $this->in_transaction = false;
    }

    /**
     * 提交事务
     */
    public function commit()
    {
        if($this->in_transaction)
        {
            $this->getPdo('write')->commit();
        }
        $this->in_transaction = false;
    }

    /**
     * 设置错误
     * @param mixed $id
     * @param string $error
     */
    public function setError(mixed $id, string $error)
    {
        $this->error = [
            'error' => $error,
            'id'    => $id
        ];
        $this->log("Error ID: {$id}\r\nMessage: {$error}");
    }

    /**
     * 获取错误信息
     * @return string
     */
    public function getError() : string
    {
        return "Error ID: {$this->error['id']}\r\nMessage: {$this->error['error']}";
    }

    /**
     * 保存日志
     * @param string $text
     * @param bool $is_execute
     */
    public function log(string $text, bool $is_execute = false)
    {
        $file = $is_execute ? $this->execute_log_dir : $this->log_file;
        if(empty($file)){
            //echo "no log file ".PHP_EOL;
            return;
        }
        if(!$is_execute){
            $dirs = explode("/", $file);
            $dirs = array_slice($dirs, 0, -1);
            $dir = join("/", $dirs);
        }else{
            $dir = $file;
            $file .= "/".date("Y-m-d").".txt";
        }
        //如果不存在文件夹
        if(!is_dir($dir)){
            @mkdir($dir, 0777, true);
        }
        $content = $is_execute ? ($text."\r\n") : (date("Y/m/d H:i:s")."\r\n".$text."\r\n\r\n");
        if($this->debug)
        {
            echo $text;
        }
        if($f = fopen($file, "a")){
            fwrite($f, $content);
            fclose($f);
        }
    }

    /**
     * 记录错误
     * @param PDOException $exception
     */
    public function logException(PDOException $exception)
    {
        $data = [
            "code: ".$exception->getCode(),
            //"info: ".$exception->errorInfo,
            "file: ".$exception->getFile(),
            "line: ".$exception->getLine(),
            "message: ".$exception->getMessage(),
            "trace: \r\n".$exception->getTraceAsString()
        ];
        $this->log(join("\r\n", $data));
    }

    /**
     * 转化关键字
     * @param mixed $fields
     * @return string
     */
    public function parseColumns(mixed $fields) : string
    {
        //[ "field1, field2", "field3", "field4, field5"]
        //"field1,field2,..."
        $fields = is_array($fields) ? explode(",", join(",", $fields)) : explode(",", $fields);
        $list = [];
        foreach ($fields as $field)
        {
            $field = trim($field);
            if(in_array($field, $this->keywords))
            {
                $field = "`{$field}`";
            }
            $list[] = $field;
        }
        return join(",", $list);
    }

    /**
     * 获取表前缀
     * @param string $type
     * @return string
     */
    public function getPrefix(string $type = 'read') : string
    {
        return $this->getTablePrefix($type);
    }
}